package org.jpropelleralt.jview.list.impl;

import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.DefaultListSelectionModel;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;

import org.jpropelleralt.box.Box;
import org.jpropelleralt.change.Change;
import org.jpropelleralt.plumbing.Source;
import org.jpropelleralt.ref.Ref;
import org.jpropelleralt.set.SetBox;
import org.jpropelleralt.utils.impl.LoggerUtils;
import org.jpropelleralt.view.View;

/**
 * A {@link ListSelectionModel} for {@link JTable} row selection,
 * that uses the {@link SetBox} of {@link Integer}s in a {@link Ref} as the 
 * selected indices.
 * 
 * TODO this does not handle filtering of the table, since then it cannot
 * select rows that have been hidden. - should probably react to this
 * by just resetting selection
 */
public class ListSelectionIndicesModel implements ListSelectionModel, View {

	private final static Logger logger = LoggerUtils.logger(ListSelectionIndicesModel.class);
	
	private final Ref<SetBox<Integer>> indexRef;
	
	/**
	 * The delegate {@link ListSelectionModel} actual provides the state
	 * and most of the handling for this {@link ListSelectionModel}. We just
	 * perform two-way synchronisation between the {@link ListSelectionModel} and
	 * our {@link Ref}.
	 */
	private final DefaultListSelectionModel delegate;

	/**
	 * This filter determines, at any point in time, whether changes in the delegate
	 * {@link ListSelectionModel} will be passed through to the {@link Ref}, or ignored.
	 */
	private final Source<Boolean> setFilter;
	
	/**
	 * The table we are working with, so we can translate row indices allowing for sorting
	 */
	private final JTable table;

	private boolean adjustingDelegate = false;

	private final Set<Integer> selection = new HashSet<Integer>();

	/**
	 * Create an {@link ListSelectionIndicesModel}
	 * 
	 * @param indexRef		The {@link Ref} giving the selected indices
	 * @param setFilter		A {@link Source} that is checked 
	 * 						whenever an attempt is made to set a new selection
	 * 						 - if this returns true, the selection is set,
	 * 						otherwise the attempt is ignored 
	 * @param table			The {@link JTable} we are working with. Must be the
	 * 						table with which this model is used.
	 */
	public ListSelectionIndicesModel(Ref<SetBox<Integer>> indexRef, Source<Boolean> setFilter, JTable table) {
		super();
		this.indexRef = indexRef;
		this.setFilter = setFilter;
		this.table = table;
		
		indexRef.features().addView(this);
		
		//Set up the delegate - multiple selection
		delegate = new DefaultListSelectionModel();
		delegate.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
		
		//Listen to the delegate - when it changes, update the ref
		//as appropriate
		delegate.addListSelectionListener(new ListSelectionListener() {
			@Override
			public void valueChanged(ListSelectionEvent e) {
				//Don't respond if selection is just adjusting
				//if (!e.getValueIsAdjusting()) {
					handleDelegateChange();
				//}
			}
		});

		//Perform initial sync from ref to delegate
		handleRefChange();
	}

	/**
	 * Respond to a change to the delegate {@link ListSelectionModel}
	 */
	private void handleDelegateChange() {
		
		//Skip when we are actually adjusting delegate in this class, so we
		//don't respond to our own changes.
		if (adjustingDelegate) return;

		//Update our cached selection
		selection.clear();
		if (!delegate.isSelectionEmpty()) {
			for (int rawIndex = delegate.getMinSelectionIndex(); rawIndex <= delegate.getMaxSelectionIndex(); rawIndex++) {
				if (delegate.isSelectedIndex(rawIndex)) {
					int index = table.convertRowIndexToModel(rawIndex);
					selection.add(index);
				}
			}
		}
		
		//Update state of ref to match selection delegate, if possible,
		//or update delegate to match Ref otherwise.
		
		//If we can set selection, update the Ref to match the delegate
		if (setFilter.get()) {
			
			//If set is different, change it
			if (!indexRef.get().equals(selection)) {
				indexRef.get().replace(selection);
			} 

		//If we are ignoring changes to the delegate, then we need to 
		//revert the delegate back to mirroring the Ref
		} else {
			handleRefChange();
		}
	}
	
	@Override
	public void changes(Map<Box, Change> changes) {
		handleRefChange();
	}
	
	/**
	 * Respond to a change to the {@link Ref}
	 */
	private void handleRefChange() {
		try {
			adjustingDelegate = true;
			
			//If the index prop has actually changed to be different from the selection,
			//then update the selection
			if (!indexRef.get().equals(selection)) {
				
				//FIXME Can we find a more elegant way to do this?
				
				//Make change as an adjustment to the delegate - this may make it more efficient (and also prevents this
				//class from responding to the changes). Would be better to
				//do this as a "batched" change, but it is hard to see how to do this
				delegate.setValueIsAdjusting(true);
	
				int originalAnchor = delegate.getAnchorSelectionIndex();
				
				delegate.clearSelection();
				
				for (Integer i : indexRef.get()) {
					try {
						int tableRow = table.convertRowIndexToView(i);
						//Only select a row if it is visible
						if (tableRow != -1) {
							delegate.addSelectionInterval(tableRow, tableRow);
						}
					} catch (IndexOutOfBoundsException ioobe) {
						//We can't select rows out of bounds
						logger.log(Level.WARNING, "Invalid model row index " + i);
					}
				}
				
				//Restore the original anchor if it is still selected
				if (delegate.isSelectedIndex(originalAnchor)) {
					delegate.setAnchorSelectionIndex(originalAnchor);
				}
				
				delegate.setValueIsAdjusting(false);
			}
		} finally {
			adjustingDelegate = false;
		}
	}

	public String toString() {
		return "ListSelectionIndicesModel using delegate: " + delegate.toString();
	}
	
	//Delegated ListSelectionModel methods
	
	public void addListSelectionListener(ListSelectionListener l) {
		delegate.addListSelectionListener(l);
	}

	public void addSelectionInterval(int index0, int index1) {
		if (!setFilter.get()) return;
		delegate.addSelectionInterval(index0, index1);
	}

	public void clearSelection() {
		if (!setFilter.get()) return;
		delegate.clearSelection();
	}

	public int getAnchorSelectionIndex() {
		return delegate.getAnchorSelectionIndex();
	}

	public int getLeadSelectionIndex() {
		return delegate.getLeadSelectionIndex();
	}

	public int getMaxSelectionIndex() {
		return delegate.getMaxSelectionIndex();
	}

	public int getMinSelectionIndex() {
		return delegate.getMinSelectionIndex();
	}

	public int getSelectionMode() {
		return delegate.getSelectionMode();
	}

	public boolean getValueIsAdjusting() {
		return delegate.getValueIsAdjusting();
	}

	public void insertIndexInterval(int index, int length, boolean before) {
		if (!setFilter.get()) return;
		delegate.insertIndexInterval(index, length, before);
	}

	public boolean isSelectedIndex(int index) {
		return delegate.isSelectedIndex(index);
	}

	public boolean isSelectionEmpty() {
		return delegate.isSelectionEmpty();
	}


	public void removeIndexInterval(int index0, int index1) {
		if (!setFilter.get()) return;
		delegate.removeIndexInterval(index0, index1);
	}

	public void removeListSelectionListener(ListSelectionListener l) {
		delegate.removeListSelectionListener(l);
	}

	public void removeSelectionInterval(int index0, int index1) {
		if (!setFilter.get()) return;
		delegate.removeSelectionInterval(index0, index1);
	}

	public void setAnchorSelectionIndex(int anchorIndex) {
		if (!setFilter.get()) return;
		delegate.setAnchorSelectionIndex(anchorIndex);
	}

	public void setLeadSelectionIndex(int leadIndex) {
		if (!setFilter.get()) return;
		delegate.setLeadSelectionIndex(leadIndex);
	}

	public void setSelectionInterval(int index0, int index1) {
		if (!setFilter.get()) return;
		delegate.setSelectionInterval(index0, index1);
	}

	public void setSelectionMode(int selectionMode) {
		if (!setFilter.get()) return;
		delegate.setSelectionMode(selectionMode);
	}

	public void setValueIsAdjusting(boolean isAdjusting) {
		if (!setFilter.get()) return;
		delegate.setValueIsAdjusting(isAdjusting);
	}
	
	//Methods in delegate but not in ListSelectionModel
//	public boolean isLeadAnchorNotificationEnabled() {
//		return delegate.isLeadAnchorNotificationEnabled();
//	}
//	public void moveLeadSelectionIndex(int leadIndex) {
//		delegate.moveLeadSelectionIndex(leadIndex);
//	}
//	public void setLeadAnchorNotificationEnabled(boolean flag) {
//		delegate.setLeadAnchorNotificationEnabled(flag);
//	}
	
}
